//
//	HenonView:View
//
//	written by Anders Bertelrud
//	(c) 1990, 1991 by Anders Bertelrud
//


#import	"HenonView.h"
#import	<dpsclient/dpsclient.h>
#import	<appkit/appkit.h>
#import	<objc/NXStringTable.h>


#define		MAXREAL		1e+10
//#define	MAXINT		65535


@implementation HenonView



////////////////////// Creation & Destruction Methods //////////////////////


- initFrame:(const NXRect *)f
{
	self = [super initFrame:f];

	//  Create, initialize, and configure a new NXImage (for use as a
	// mapping cache).
	henonCache = [[NXImage alloc] initSize:&f->size];
	[henonCache useCacheWithDepth:NX_TwoBitGrayDepth];

	useGrid = YES;
	continuousPlot = YES;
	isRunning = NO;

	timerTicking = NO;

	// Initialize the default plot values, and init the mapping cache.
	[self setPlotValues1:self];
	[self initHenonCache];

	return self;
}



- free
{
	[self stopTimer];
	[henonCache free];

	[super free];
	return nil;
}



//////////////////////////// Rendering Methods /////////////////////////////


- drawSelf:(const NXRect *)rects :(int)rectCount
{
	//  Since the mapping is cached, all we have to do is to composite
	// the cache into the panel.
	[henonCache composite:NX_COPY toPoint:&bounds.origin];

	return self;
}



/////////////////////////////// File Methods ///////////////////////////////


- saveAsTIFF:sender
{
	id			savePanel = [SavePanel new];
	NXStream	*tiffStream;

	//  This method should probably allow the user to specify compression
	// type and (in the case of JPEG) quantization value. This could be
	// done using an accessory view.
	[savePanel setRequiredFileType:"tiff"];
	if ([savePanel runModal])
	{
		if ((tiffStream = NXOpenMemory(NULL, 0, NX_WRITEONLY)) == NULL)
		{
			NXRunAlertPanel([stringTable valueForStringKey:"FS error"],
				[stringTable valueForStringKey:"Couldn't write TIFF to"],
				[stringTable valueForStringKey:"OK"], NULL, NULL,
				[savePanel filename]);
		}
		else
		{
			[henonCache writeTIFF:tiffStream];
			NXSaveToFile(tiffStream, [savePanel filename]);
			NXCloseMemory(tiffStream, NX_FREEBUFFER);
		}
	}

	return self;
}



////////////////////////////// Action Methods //////////////////////////////


- startStop:sender
{
	if (!isRunning)
	{
		// We aren't already plotting a map, so start now.

		L = [[lField cell] floatValue];
		T = [[tField cell] floatValue];
		R = [[rField cell] floatValue];
		B = [[bField cell] floatValue];

		//  Now check values and stuff. It is really very bad design philo-
		// sophy to yell at the user using alert panels; this is just a cheap
		// demo, however, so we'll leave it at this for now. At least we're
		// using string tables to make internationalization easy.

		if (L >= R)
		{
			NXRunAlertPanel([stringTable valueForStringKey:"Range error"],
				[stringTable valueForStringKey:"Right must be > left"],
				[stringTable valueForStringKey:"OK"], NULL, NULL);
			return self;
		}

		if (B >= T)
		{
			NXRunAlertPanel([stringTable valueForStringKey:"Range error"],
				[stringTable valueForStringKey:"Top must be > bottom"],
				[stringTable valueForStringKey:"OK"], NULL, NULL);
			return self;
		}

		orbits = [[orbitField cell] intValue];
		if (orbits < 1)
		{
			NXRunAlertPanel([stringTable valueForStringKey:"Range error"],
				[stringTable valueForStringKey:"# orbits must be int >= 1"],
				[stringTable valueForStringKey:"OK"], NULL, NULL);
			return self;
		}

		points = [[pointsField cell] intValue];
		if (points < 1)
		{
			NXRunAlertPanel([stringTable valueForStringKey:"Range error"],
				[stringTable valueForStringKey:"# pts/orbit must be int >= 1"],
				[stringTable valueForStringKey:"OK"], NULL, NULL);
			return self;
		}

		dy0 = [[incYField cell] floatValue];
		dx0 = [[incXField cell] floatValue];
		y0 = [[startYField cell] floatValue];
		x0 = [[startXField cell] floatValue];

		phase = [[phaseField cell] floatValue];
		if (phase < 0 || phase > 3.141593)
		{
			NXRunAlertPanel([stringTable valueForStringKey:"Range error"],
				[stringTable valueForStringKey:"Phase angle must be 0<=a<=pi"],
				[stringTable valueForStringKey:"OK"], NULL, NULL);
			return self;
		}

		// Calculate some things used by the caculation algorithm.
		cosa = cos(phase);
		sina = sin(phase);
		xOld = x0;
		yOld = y0;
		xScale = bounds.size.width / (R - L);
		yScale = bounds.size.height / (T - B);

		currentOrbit = 1;
		currentPoint = 1;

		isRunning = YES;

		// Start the plot.
		[[currentOrbitField cell] setIntValue:currentOrbit];
		[self initHenonCache];
		[self display];

		// Start the timer.
		[self startTimer];

		[nextOrbitButton setEnabled:YES];
		[startStopButton setTitle:[stringTable valueForStringKey:"Stop"]];
	}
	else
	{
		// We're plotting a map, so stop now that the button has been clicked.

		isRunning = NO;
		[self stopTimer];
		[nextOrbitButton setEnabled:NO];
		[[currentOrbitField cell] setStringValue:""];
		[startStopButton setTitle:[stringTable valueForStringKey:"Plot"]];
	}
	return self;
}



- gridChecked:sender
{
	useGrid = [sender state];
	return self;
}



- nextOrbit:sender
{
	//  This is a quick way to make sure that we'll go into the next orbit
	// the next time doHenonCalculation is called.
	currentPoint = points + 1;
	return self;
}



- setPlotValues1:sender
{
	isRunning = YES;		// Hack so that startStop: will do the right thing.
	[self startStop:self];	// Stop the current plot.

	phase = 1.111;		[phaseField setFloatValue:phase];
						[[phaseSlider cell] setFloatValue:phase];
	L = -1.2;			[lField setFloatValue:L];
	T = 1.2;			[tField setFloatValue:T];
	R = 1.2;			[rField setFloatValue:R];
	B = -1.2;			[bField setFloatValue:B];
	x0 = 0.098;			[startXField setFloatValue:x0];
	y0 = 0.061;			[startYField setFloatValue:y0];
	dx0 = 0.04;			[incXField setFloatValue:dx0];
	dy0 = 0.03;			[incYField setFloatValue:dy0];
	orbits = 38;		[orbitField setIntValue:orbits];
	points = 700;		[pointsField setIntValue:points];

	return self;
}



- setPlotValues2:sender
{
	isRunning = YES;		// Hack so that startStop: will do the right thing.
	[self startStop:self];	// Stop the current plot.

	phase = 0.264;		[phaseField setFloatValue:phase];
						[[phaseSlider cell] setFloatValue:phase];
	L = -1.2;			[lField setFloatValue:L];
	T = 1.2;			[tField setFloatValue:T];
	R = 1.2;			[rField setFloatValue:R];
	B = -1.2;			[bField setFloatValue:B];
	x0 = 0.098;			[startXField setFloatValue:x0];
	y0 = 0.061;			[startYField setFloatValue:y0];
	dx0 = 0.04;			[incXField setFloatValue:dx0];
	dy0 = 0.03;			[incYField setFloatValue:dy0];
	orbits = 25;		[orbitField setIntValue:orbits];
	points = 300;		[pointsField setIntValue:points];

	return self;
}



- setPlotValues3:sender
{
	isRunning = YES;		// Hack so that startStop: will do the right thing.
	[self startStop:self];	// Stop the current plot.

	phase = 1.5732;		[phaseField setFloatValue:phase];
						[[phaseSlider cell] setFloatValue:phase];
	L = -3.0;			[lField setFloatValue:L];
	T = 3.0;			[tField setFloatValue:T];
	R = 3.0;			[rField setFloatValue:R];
	B = -3.0;			[bField setFloatValue:B];
	x0 = 0.098;			[startXField setFloatValue:x0];
	y0 = 0.061;			[startYField setFloatValue:y0];
	dx0 = 0.04;			[incXField setFloatValue:dx0];
	dy0 = 0.03;			[incYField setFloatValue:dy0];
	orbits = 70;		[orbitField setIntValue:orbits];
	points = 400;		[pointsField setIntValue:points];

	return self;
}



////////////////////////////// Timer Methods ///////////////////////////////


// The timed entry function.
void	timedEntryFunction (DPSTimedEntry entryID, double currentTime, id self)
{
	[self doHenonCalculation];
}



- startTimer
{
	[self stopTimer];
	timerTicking = YES;
	timer = DPSAddTimedEntry((double)0.0,
		(DPSTimedEntryProc)&timedEntryFunction, self, NX_BASETHRESHOLD);
	return self;
}



- stopTimer
{
	
	if (timerTicking)
	{
		DPSRemoveTimedEntry(timer);
		timerTicking = NO;
	}
	return self;
}



////////////////////////// Miscellaneous Methods ///////////////////////////


- doHenonCalculation
{
	float	xNew, yNew;

	//  If you want to modify the plot algorithm, this is the place to do it.
	// This method is called continuously (from the timed entry function), and
	// it calculates exactly one point in the HÝnon map. The location of the
	// point is based on the location of the previous point as well as on the
	// values given in the plot panel.
	//  If, after calculating the current point, we have calculated all the
	// points in the orbit, we update the HÝnon window and start the next
	// orbit.

	if ([henonCache lockFocus])
	{
		PSsetgray(NX_BLACK);
		if (currentPoint <= points)
		{
			if (abs(xOld) < MAXREAL && abs(yOld) < MAXREAL)
			{
				xNew = xOld*cosa - (yOld - xOld*xOld)*sina;
				yNew = xOld*sina + (yOld - xOld*xOld)*cosa;

				if (abs(xNew - L) < MAXINT/xScale
					&& abs(T - yNew) < MAXINT/yScale)
				{
					float	x, y;

					x = (xNew - L) * xScale;
					y = (T - yNew) * yScale;
					if (x >= bounds.origin.x
						&& x <= bounds.origin.x + bounds.size.width
					    && y >= bounds.origin.y
						&& y <= bounds.origin.y + bounds.size.height)
					{
						PSmoveto(x, y);
						PSrlineto(0, 0);
						PSstroke();
					}
				}

				xOld = xNew;
				yOld = yNew;
			}
		}
		else
		{
			if (currentOrbit < orbits)
			{
				// Calculate next starting point.
				xOld = x0 + currentOrbit * dx0;
				yOld = y0 + currentOrbit * dy0;

				//  Reset the current point, and update the TextField in
				// the HÝnon panel to reflect this change.
				currentPoint = 1;
				[[currentOrbitField cell] setIntValue:++currentOrbit];

				// Update the panel to show the just completed orbit.
				[self lockFocus];
				[henonCache composite:NX_COPY toPoint:&bounds.origin];
				[self unlockFocus];
				[window flushWindow];
			}
			else
			{
				// We're now finished with the plot, so clean things up.
				[self stopTimer];
				isRunning = NO;
				[startStopButton setTitle:"Plot"];
				[nextOrbitButton setEnabled:NO];
				[[currentOrbitField cell] setStringValue:""];
			}
		}

		//  Set things up so that the next point will be calculated the next
		// time the timed entry calles doHenonCalculation.
		++currentPoint;

		[henonCache unlockFocus];
	}

	return self;
}



- initHenonCache
{
	float		bR, bT, bL, bB, bW, bH;
	NXPoint	origin;

	[henonCache lockFocus];

	// Figure out shorthands for some edge values.
	bL = bounds.origin.x;
	bT = bounds.origin.y + bounds.size.height;
	bR = bounds.origin.x + bounds.size.width;
	bB = bounds.origin.y;
	bW = bounds.size.width;
	bH = bounds.size.height;

	PSsetgray(NX_WHITE);
	NXRectFill(&bounds);

	origin.x = (0.0 - L) * bW / (R - L);
	origin.y = (0.0 - B) * bH / (T - B);

	if ((R - L) < R && (T - B) < T)
	{
		origin.x = bR - 10;
		origin.x = bB + 10;
	}

	// If the user wants a grid, draw one.
	if (useGrid)
	{
		PSmoveto(0, origin.y);
		PSlineto(bR, origin.y);
		PSmoveto(origin.x, 0);
		PSlineto(origin.x, bT);

		PSsetgray(NX_DKGRAY);
		PSsetlinewidth(0);
		PSstroke();
	}

	[henonCache unlockFocus];
	return self;
}



//////////////////////////// Delegation Methods ////////////////////////////


- appDidInit:sender
{
	[window makeKeyAndOrderFront:self];
	return self;
}



@end